from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import datetime  # For datetime objects
import os.path  # To manage paths
from pickle import FALSE
import sys  # To find out the script name (in argv[0])
import csv  # To write to CSV file
import pandas as pd  # To sort CSV file
from bk_pull_akshare import download_stock_data_from_stocklist, read_list_file
from bk_emal import example_send_email_with_attachment
sys.path.append('./backtrader')
# data_path_global = './backtrader/datas/orcl-1995-2014.txt'

# Import the backtrader platform
import backtrader as bt

current_date = datetime.datetime.now().strftime("%Y%m%d")
csv_filename = f"./report_out/bcg_buy_signals_{current_date}.txt"#可以手动行为csv后缀

# Create a Stratey
class TestStrategy(bt.Strategy):
    params = (
        ('maperiod_2', 2),
        ('maperiod_5', 5),
        ('maperiod_10', 10),
        ('maperiod_20', 20),
        ('maperiod_30', 30),
        ('maperiod_60', 60),
        ('maperiod_120', 120),
        ('divergence_period', 90),  # 底背离判断的天数
        ('top_divergence_period', 60),  # 顶背离判断的天数
        ('sell_ma_period', 10),  # 卖出信号均线天数
        ('profit_take_pct', 5.0),  # 止盈百分比（3%）
        ('stock_name', ''),  # 股票名称

    )

    def log(self, txt, dt=None):
        ''' Logging function fot this strategy'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        # Keep a reference to the "close" line in the data[0] dataseries
        self.dataclose = self.datas[0].close
        # 引用成交量数据
        self.volume = self.datas[0].volume
       
        # To keep track of pending orders and buy price/commission
        self.order = None
        self.buyprice = None
        self.buycomm = None
        
        # 统计变量
        self.trade_count = 0  # 交易次数
        self.start_cash = None  # 初始资金
        self.start_date = None  # 开始日期
        self.end_date = None  # 结束日期
        
        # 保存最后一次买入信号信息
        self.last_buy_signal = None
        
        # 初始化股票名称变量
        self.stock_name = ""

        # Add a MovingAverageSimple indicator
        self.sma = bt.indicators.SimpleMovingAverage(
            self.datas[0], period=self.params.maperiod_5)
        self.sma10 = bt.indicators.SimpleMovingAverage(
            self.datas[0], period=self.params.maperiod_10)
        self.sma20 = bt.indicators.SimpleMovingAverage(
            self.datas[0], period=self.params.maperiod_20)
        self.sma30 = bt.indicators.SimpleMovingAverage(
            self.datas[0], period=self.params.maperiod_30)
        # 添加120日均线
        self.sma120 = bt.indicators.SimpleMovingAverage(
            self.datas[0], period=self.params.maperiod_120)
        # 添加5日线上穿120日线的交叉指标
        self.crossover_5_120 = bt.ind.CrossOver(self.sma, self.sma120)

        # bt.indicators.StochasticSlow(self.datas[0])
        self.macd_histo = bt.indicators.MACDHisto(self.datas[0])
        # add diff and dea indicator
        self.macd = bt.indicators.MACD(self.datas[0])
        # MACD包含了三个值：macd (DIFF线), signal (DEA线), histo (柱状图)
        self.diff = self.macd.macd  # DIFF线 (快速EMA - 慢速EMA)
        self.dea = self.macd.signal  # DEA线 (DIFF的EMA平滑线)
        
        # 添加最低价指标用于底背离判断
        self.lowest_period = bt.indicators.Lowest(self.datas[0].low, period=self.params.divergence_period)
        
        # 添加最高价指标用于顶背离判断
        self.highest_period = bt.indicators.Highest(self.datas[0].high, period=self.params.top_divergence_period)
        
        # 添加卖出信号用的均线指标
        self.sell_ma = bt.indicators.SimpleMovingAverage(
            self.datas[0], period=self.params.sell_ma_period)
        
        # 初始化股票名称变量
        self.stock_name = self.params.stock_name

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            return

        # Check if an order has been completed
        # Attention: broker could reject order if not enough cash
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                    (order.executed.price,
                     order.executed.value,
                     order.executed.comm))

                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
            else:  # Sell
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order.executed.price,
                          order.executed.value,
                          order.executed.comm))

            self.bar_executed = len(self)

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')

        self.order = None

    def notify_trade(self, trade):
        if not trade.isclosed:
            return

        # 统计交易次数
        self.trade_count += 1
        
        self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
                 (trade.pnl, trade.pnlcomm))


    def start(self):
        # 记录开始信息
        self.start_cash = self.broker.getvalue()
        # 暂时将start_date设为None，稍后在next方法中设置
        self.start_date = None
        self.log('策略开始运行 - 初始资金: %.2f' % self.start_cash)
        
    def stop(self):
        # 记录结束信息
        self.end_date = self.datas[0].datetime.date(0)
        final_cash = self.broker.getvalue()
        
        
        # 保存最后一次买入信号到CSV文件
        if self.last_buy_signal:
            # 获取数据文件名作为股票代码，只提取数字部分
            data_name = self.datas[0]._name if self.datas[0]._name else "unknown"
            # 从格式如"600016_stock_data"中提取数字部分
            if data_name != "unknown" and "_stock_data" in data_name:
                stock_code = data_name.split("_stock_data")[0]
            else:
                stock_code = data_name
            
            # 在文件名中增加当前日期

            
            # 检查文件是否存在
            import os
            file_exists = os.path.isfile(csv_filename)
            
            # 写入CSV文件（存在则追加，不存在则新建）
            with open(csv_filename, 'a', newline='', encoding='utf-8') as csvfile:
                fieldnames = ['datetime', 'stock_code', 'stock_name', 'price', 'operate', 'new_price']
                writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
                
                # 如果是新文件，先写入表头
                if not file_exists:
                    writer.writeheader()
                # 添加operate字段并填充为buy
                self.last_buy_signal['operate'] = 'buy'
                # 使用提取的股票代码
                self.last_buy_signal['stock_code'] = stock_code
                # 添加股票名称
                self.last_buy_signal['stock_name'] = self.stock_name
                # 添加最后一天的收盘价
                self.last_buy_signal['new_price'] = round(self.dataclose[0], 2)
                writer.writerow(self.last_buy_signal)
            
            self.log(f'最后一次买入信号已保存到 {csv_filename}')
        
        # 计算统计数据
        total_return = ((final_cash - self.start_cash) / self.start_cash) * 100
        
        # 计算交易周期
        if self.start_date and self.end_date:
            trade_period = (self.end_date - self.start_date).days
            # 确保交易周期不为负数
            if trade_period < 0:
                trade_period = 0
        else:
            # 如果没有有效的开始和结束日期，使用默认值0
            trade_period = 0
        
        # self.log('='*50)
        # self.log('策略统计结果:')
        # self.log('交易周期: %s 至 %s (共%d天)' % (self.start_date, self.end_date, trade_period))
        # self.log('初始资金: %.2f' % self.start_cash)
        # self.log('最终资金: %.2f' % final_cash)
        # self.log('总收益率: %.2f%%' % total_return)
        # self.log('交易次数: %d' % self.trade_count)
        # if trade_period > 0:
        #     self.log('年化收益率: %.2f%%' % (total_return * 365 / trade_period))
        # self.log('='*50)
    def check_buymacd(self):
        # 判断DIFF和DEA在零轴下方形成金叉的条件
        # 金叉：DIFF线从下方穿越DEA线
        # 零轴下方：当前DIFF和DEA都小于0
        # 需要至少2个数据点来判断穿越
        if (len(self) > 1 and 
            self.diff[0] < 0 and self.dea[0] < 0 and  # 当前都在零轴下方
            self.diff[0] > self.dea[0] and  # 当前DIFF在DEA上方
            self.diff[-1] <= self.dea[-1]):  # 前一根K线DIFF在DEA下方或相等
            # self.log('*** 金叉信号：DIFF线在零轴下方穿越DEA线！DIFF: %.2f, DEA: %.2f ***' % 
            #         (self.diff[0], self.dea[0]))
            # self.order = self.buy()
            
            return  1
        return 0
    def check_sell_macd(self):
        # 判断DIFF和DEA在零轴上方形成死叉的条件
        # 死叉：DIFF线从上方穿越DEA线
        # 零轴上方：当前DIFF和DEA都大于0
        # 需要至少2个数据点来判断穿越
        if (len(self) > 1 and 
            self.diff[0] > 0 and self.dea[0] > 0 and  # 当前都在零轴上方
            self.diff[0] < self.dea[0] and  # 当前DIFF在DEA下方
            self.diff[-1] >= self.dea[-1]):  # 前一根K线DIFF在DEA上方或相等
            # self.log('*** 死叉信号：DIFF线在零轴上方穿越DEA线！DIFF: %.2f, DEA: %.2f ***' % 
            #         (self.diff[0], self.dea[0]))
            # self.order = self.sell()
            return  1
        return 0
    def check_buymacd_diff(self):
        if (len(self) > 1 and 
            self.diff[0] > 0 and  # 当前DIFF在零轴上方
            self.diff[-1] <= 0):  # 前一根K线DIFF在零轴下方或等于零轴
            # self.log('*** 强势信号：DIFF线上穿零轴！DIFF: %.2f, DEA: %.2f ***' % 
            #         (self.diff[0], self.dea[0]))
            # self.order = self.buy()
            return  1
        return 0
    def check_buy_Deviation(self):
        """底背离买入判断函数
        当价格达到指定期间最低价但DIFF没有创新低时（底背离）执行买入
        """
        # 需要至少指定天数的数据点来计算最低价
        if len(self) >= self.params.divergence_period:
            # 当前价格是否等于指定期间最低价
            current_low = self.datas[0].low[0]
            lowest_period_value = self.lowest_period[0]
            
            # 容忍小误差，判断当前低价是否接近指定期间最低价
            if abs(current_low - lowest_period_value) < 0.01:
                # 计算指定期间内DIFF的最低值
                diff_period_min = float('inf')
                for i in range(min(self.params.divergence_period, len(self))):
                    if self.diff[-i] < diff_period_min:
                        diff_period_min = self.diff[-i]
                
                # 判断当前DIFF是否高于指定期间内的最低值（底背离）
                if self.diff[0] > diff_period_min:
                    # self.log('*** 底背离信号：价格创%d日新低但DIFF未创新低！Price: %.2f, DIFF: %.2f, %d日DIFF最低: %.2f ***' % 
                            # (self.params.divergence_period, current_low, self.diff[0], self.params.divergence_period, diff_period_min))
                    # 如果没有仓位，执行买入操作
                    # if not self.position:
                    # self.order = self.buy()
                    return 1
        return 0
    def check_buy_sma_vol(self):
        """判断成交量3日内递增、5日线上穿120日线、价格回到5日线的买入信号函数"""
        # 需要至少3个数据点来判断成交量递增
        # 需要至少120个数据点来确保120日均线有效
        if len(self) >= 120:
            # 条件1：成交量3日内递增
            # 当前成交量 > 前一天成交量 > 前两天成交量
            volume_increasing = (self.volume[0] > self.volume[-1] and 
                                 self.volume[-1] > self.volume[-2])
            
            # 条件2：5日线上穿120日线（最近几天内发生的金叉）
            # 使用交叉指标检测最近3天内是否发生了金叉
            crossover_occurred = False
            for i in range(3):
                if len(self) > i and self.crossover_5_120[-i] > 0:
                    crossover_occurred = True
                    break
            
            # 条件3：价格回到5日线（当前价格接近5日线）
            # 定义一个较小的阈值来判断价格是否接近5日线
            price_near_sma5 = abs(self.dataclose[0] - self.sma[0]) < self.dataclose[0] * 0.005
            
            # 当三个条件都满足时返回1，否则返回0
            if volume_increasing and crossover_occurred and price_near_sma5:
                return 1
        return 0

    def check_sell_Deviation(self):
        """顶背离卖出判断函数
        当价格达到指定期间最高价但DIFF没有创新高时（顶背离）返回1，否则返回0
        """
        # 需要至少指定天数的数据点来计算最高价
        if len(self) >= self.params.top_divergence_period:
            # 当前价格是否接近指定期间最高价（考虑小误差）
            current_high = self.datas[0].high[0]
            highest_period_value = self.highest_period[0]
            
            # 容忍小误差，判断当前高价是否接近指定期间最高价
            if abs(current_high - highest_period_value) < 0.01:
                # 计算指定期间内DIFF的最高值
                diff_period_max = float('-inf')
                for i in range(min(self.params.top_divergence_period, len(self))):
                    if self.diff[-i] > diff_period_max:
                        diff_period_max = self.diff[-i]
                
                # 判断当前DIFF是否低于指定期间内的最高值（顶背离）
                if self.diff[0] < diff_period_max:
                    return 1
        return 0
    def next(self):
        # 记录第一个交易日作为开始日期
        if self.start_date is None:
            self.start_date = self.datas[0].datetime.date(0)
            self.log('策略开始日期记录: %s' % self.start_date)
            
        # Simply log the closing price of the series from the reference
        # self.log('Close_day: %.2f, sma: %.2f, sma10: %.2f, sma20: %.2f, DIFF: %.2f, DEA: %.2f, macd_histo: %.2f' % 
        #         (self.dataclose[0], self.sma[0], self.sma10[0], self.sma20[0], self.diff[0], self.dea[0], self.macd_histo.histo[0]))
                # DIFF和DEA指标已添加并打印 
        
        
        
        # Check if an order is pending ... if yes, we cannot send a 2nd one
        if self.order:
            return
        #==================================================================
        if self.check_buymacd(): # 调用金叉买入判断函数
            # self.order = self.buy()
            # self.log('*** 金叉买入信号：DIFF线在零轴下方穿越DEA线！DIFF: %.2f, DEA: %.2f ***' % 
            #         (self.diff[0], self.dea[0]))
            # 保存最后一次买入信号信息
            dt = self.datas[0].datetime.date(0)
            data_name = self.datas[0]._name if self.datas[0]._name else "unknown"
            price = self.dataclose[0]
            self.last_buy_signal = {
                'datetime': dt,
                'stock_code': data_name,
                'price': round(price, 2)
            }
            pass
        # ====================卖出操作================
        if self.position:
            # 死叉卖出信号
            if self.check_sell_macd():
                self.log('*** 死叉卖出信号：DIFF线在零轴上方穿越DEA线！DIFF: %.2f, DEA: %.2f ***' % 
                        (self.diff[0], self.dea[0]))
                self.order = self.sell()
                pass
def check_data_file_count(data_path):
    # 打开data_path csv文件, 检查数据量是否超过150条
    try:
        with open(data_path, 'r', encoding='utf-8') as f:
            # 读取所有行并计算行数
            lines = f.readlines()
            # 减去1是因为第一行是表头
            data_count = len(lines) - 1
            print(f"数据文件 {data_path} 包含 {data_count} 条数据")
            
            # 检查数据量是否超过150条
            if data_count <= 150:
                print(f"警告: 数据量不足150条({data_count}条)，可能影响策略执行效果")
                # 可以选择是否继续执行，这里我们继续执行
                return False
            else:
                return True
    except FileNotFoundError:
        print(f"错误: 数据文件 {data_path} 不存在")
        return False
    except Exception as e:
        print(f"读取数据文件时出错: {e}")
        return False
def bk_stratey_macd_diff(data_path, from_date=None, to_date=None, stock_name=""):
    # Create a cerebro entity
    cerebro = bt.Cerebro()

    # Add a strategy with stock name parameter
    cerebro.addstrategy(TestStrategy, stock_name=stock_name)

    # Datas are in a subfolder of the samples. Need to find where the script is
    # because it could have been called from anywhere
    modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
    datapath = os.path.join(modpath, data_path)

    # 检查数据文件是否存在
    if not os.path.exists(datapath):
        print(f"错误: 数据文件 {datapath} 不存在")
        return

    # Create a Data Feed
    try:
        data = bt.feeds.YahooFinanceCSVData(
            dataname=datapath,
            # Do not pass values before this date
            fromdate=from_date,
            # Do not pass values after this date
            todate=to_date,
            # Do not pass values after this date
            reverse=False)
    except Exception as e:
        print(f"创建数据源时出错: {e}")
        return

    # Add the Data Feed to Cerebro
    try:
        cerebro.adddata(data)
    except Exception as e:
        print(f"添加数据源到Cerebro时出错: {e}")
        return

    # Set our desired cash start
    cerebro.broker.setcash(10000000.0)

    # Add a FixedSize sizer according to the stake
    cerebro.addsizer(bt.sizers.FixedSize, stake=1)

    # Set the commission
    cerebro.broker.setcommission(commission=0.05)

    # Print out the starting conditions
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

    # Run over everything
    try:
        cerebro.run()
    except Exception as e:
        print(f"运行策略时出错: {e}")
        import traceback
        traceback.print_exc()
        return

    # Print out the final result
    final_value = cerebro.broker.getvalue()
    print('Final Portfolio Value: %.2f' % final_value)
    
    
    # Plot the result
    # cerebro.plot()

def check_all_by_stocklist_file(stock_list, stock_dict):
    pass
    # stock_list = []  # 存储股票信息的列表
    # stock_dict = {}  # 存储股票编码到名称的映射字典
    # # 使用read_list_file函数读取股票列表文件
    # stock_list, stock_dict = read_list_file("Table.txt")
    
    # 增加保存错误股票编码的变量
    error_stocks = []
    handle_ct_err = 0
    
    # 处理每只股票
    for code, name in stock_list:
        # 确保股票代码不为空
        if code:
            try:
                # 构造数据文件路径
                data_path_global = f'./csv_data/{code}_stock_data.csv'
                to_date = datetime.datetime.now()
                # 修改from_date为to_date减去, #这里注意要满足均线周期最小天数,需要的是交易日的天数数据
                from_date = to_date - datetime.timedelta(days=365)
                print(f"backtest: {data_path_global}")
                
                # 检查数据文件是否存在
                if not os.path.exists(data_path_global):
                    print(f"警告: 数据文件 {data_path_global} 不存在，跳过该股票")
                    error_stocks.append(code)  # 保存错误股票编码
                    handle_ct_err = handle_ct_err + 1
                    # print(f"数据文件 {data_path_global} 不满足条件，跳过")
                    continue
                
                # 使用函数检测数据量是否足够
                if not check_data_file_count(data_path_global):
                    print(f"警告: 股票 {code} 数据量不足150条，跳过该股票")
                    error_stocks.append(code)  # 保存错误股票编码
                    handle_ct_err = handle_ct_err + 1
                    continue
                
                # 获取股票名称
                stock_name = stock_dict.get(code, "")
                bk_stratey_macd_diff(data_path_global, from_date, to_date, stock_name)
            except Exception as e:
                print(f"处理股票 {code} 时出错: {e}")
                error_stocks.append(code)  # 保存错误股票编码
                handle_ct_err = handle_ct_err + 1
                continue
    
    # 最后结束时打印错误股票编码
    if error_stocks:
        print("\n以下股票处理出错或数据不足:")
        for stock in error_stocks:
            print(f"  {stock}")
        print(f"总共 {len(error_stocks)} 只股票处理失败")
    else:
        print("\n所有股票处理完成，无错误")
def  test_stratey_macd_diff():

    # 可以通过参数传递日期范围，如果不传递则使用默认值
    to_date = datetime.datetime.now()
    # 修改from_date为to_date减去, #这里注意要满足均线周期最小天数,需要的是交易日的天数数据
    from_date = to_date - datetime.timedelta(days=365)
    print(f"数据日期范围: {from_date} - {to_date}")
    # 000001_stock_data 000063_stock_data 600188_stock_data 
    # data_path_global = f'./csv_data/000001_stock_data.csv'
    data_path_global = f'./csv_data/600188_stock_data.csv'
    bk_stratey_macd_diff(data_path_global, from_date, to_date)
def sort_csv_file(csv_file_path):
    """对CSV文件进行排序"""
    # 读取CSV文件
    df = pd.read_csv(csv_file_path)

    # 将日期列转换为日期格式
    df['datetime'] = pd.to_datetime(df['datetime'])

    # 对日期列进行降序排序
    df.sort_values(by='datetime', ascending=False, inplace=True)
    
    # 将排序后的数据,和当前日期比较, 删除10天前的数据
    # 获取当前日期
    current_date = datetime.datetime.now()
    # 计算10天前的日期
    ten_days_ago = current_date - datetime.timedelta(days=20)
    # 删除10天前的数据
    df = df[df['datetime'] >= ten_days_ago]
    
    # 保存排序后的数据回CSV文件
    df.to_csv(csv_file_path, index=False)

def  macd_diff_strategy_run():
    
    # 使用read_list_file函数读取股票列表文件
    stock_list, stock_dict = read_list_file("Table.txt")
    
    download_stock_data_from_stocklist(stock_list)#更新数据

    # test_stratey_macd_diff()
    #检查文件 csv_filename 是否存在 ,存在则删除
    file_exists = os.path.isfile(csv_filename)
    if file_exists:
        os.remove(csv_filename)
        print(f"已删除文件: {csv_filename}")
    
    check_all_by_stocklist_file(stock_list, stock_dict)#批量处理
    sort_csv_file(csv_filename)#排序
    # 发送邮件
    example_send_email_with_attachment([csv_filename])

if __name__ == '__main__':
    macd_diff_strategy_run()
    